Cargo workspace与monorepo工程组织
1. 这是什么
当 Rust 项目还很小的时候,一个 crate 往往就够了。
但一旦项目开始变大,很快就会遇到这些情况:
- 一个仓库里不止一个二进制程序
- 需要共享公共库代码
- Web 服务、CLI、工具脚本、测试工具想放在一起维护
- 团队希望统一依赖、统一版本、统一 CI
这时就会进入一个更工程化的话题:
- Cargo workspace 与 monorepo 工程组织
一句话理解:
workspace解决的是“多个 Rust crate 怎样作为一个整体协同开发”monorepo解决的是“多个相关项目怎样在一个仓库里被统一组织和治理”
2. 为什么这件事重要
因为很多项目的复杂度,往往不是先来自语言本身,
而是先来自代码组织方式。
如果组织方式不清楚,常见问题会很快出现:
- 共享代码被复制粘贴
- 依赖版本彼此漂移
- 构建和测试范围越来越难控制
- 发布某个组件时影响面不清楚
- 团队不知道什么应该放在公共层,什么应该放在业务层
所以 workspace 不是“仓库稍微大一点时的语法功能”,
而是 Rust 工程化进入中大型项目后的基础设施。
3. 先建立直觉
3.1 crate 是代码单元,workspace 是协作单元
单个 crate 更像是:
- 一个库
- 一个可执行程序
- 一个独立编译与发布单元
而 workspace 更像是:
- 多个 crate 的协作容器
它让这些 crate 能够:
- 在同一个锁文件下管理依赖
- 共享一部分配置
- 统一构建与测试
- 形成更清楚的仓库边界
3.2 monorepo 不是“全放一起”,而是“边界清晰地放一起”
很多人第一次理解 monorepo 时,会误以为它只是:
- 把所有东西塞进一个仓库
但真正有价值的 monorepo 并不是混乱堆叠,
而是:
- 在一个仓库中维护多个有关联但边界清楚的模块
也就是说,monorepo 的价值不是模糊边界,
而是让依赖关系、共享资产和协同流程更可见。
4. Cargo workspace 真正解决什么问题
4.1 统一依赖与锁文件
当多个 crate 分开维护时,经常会遇到:
- 同一依赖版本不一致
- 本地调得通,CI 行为却不稳定
- 修一个安全漏洞需要逐个更新
workspace 的重要价值之一,就是把多个 crate 放进一个更统一的依赖管理范围。
4.2 共享基础库与基础设施代码
真实项目里通常会有一些公共能力:
- 配置模型
- 错误类型
- 领域模型
- 数据访问基础层
- 通用工具函数
- 协议定义
这些能力如果散落复制,很快就会变成维护灾难。
workspace 让你能把它们更自然地提取成共享 crate。
4.3 统一构建、测试与质量治理
随着 crate 数量增加,团队更关心的通常不是“能不能编译”,
而是:
- 怎么统一测试
- 怎么统一 lint
- 怎么统一格式化
- 怎么统一依赖策略
workspace 的工程价值,很多时候就体现在这些治理动作能否规模化执行。
5. Rust 项目为什么容易走向 workspace
Rust 生态鼓励明确边界:
- crate 边界清楚
- 依赖关系显式
- 库与二进制程序区分自然
- 共享逻辑适合被提炼成独立 crate
所以当项目开始成长时,Rust 很自然就会从“单 crate”演化到“多 crate workspace”。
这不是过度设计,而往往是代码体量增长后的自然结果。
6. 什么时候该考虑 monorepo
6.1 当多个组件高度协同时
例如:
- 一个 API 服务
- 一个后台任务 worker
- 一个 CLI 管理工具
- 一个共享 domain / types crate
- 一个 SDK 或内部 client
如果这些组件生命周期强相关、经常一起改动,放在一个 monorepo 往往更容易协同。
6.2 当你需要统一治理工程规范时
monorepo 更适合处理:
- 统一 CI
- 统一依赖升级
- 统一发布流程
- 统一安全扫描
- 统一代码风格与质量门禁
也就是说,monorepo 的收益不仅在代码复用,
也在流程治理。
7. 真实工程里真正该建立的能力
7.1 区分“共享代码”与“耦合代码”
不是所有重复看起来都应该抽公共 crate。
一个成熟工程要能分辨:
- 哪些是真正稳定、值得复用的共享能力
- 哪些只是暂时相似,但抽出来会造成反向耦合
workspace 能帮助拆分,但不会自动帮你做出正确抽象。
7.2 让 crate 边界反映架构边界
如果 crate 的划分只是按文件数量随手切,后续仍会混乱。
更好的思路通常是:
- 让 crate 边界尽量贴近领域边界、运行单元边界或基础设施边界
例如:
app-apiapp-workerdomaininfra-dbcommon-config
重点不在名字,而在边界语义是否清晰。
7.3 不要把 workspace 当成“大而全公共桶”
很多团队一旦上 workspace,就容易把各种工具函数都堆进一个 common 或 utils crate。
这样短期方便,长期却容易变成隐式耦合中心。
所以 workspace 成功与否,不在于 crate 多不多,
而在于:
- 共享是否克制,边界是否稳定
8. 常见误区
8.1 误区一:项目一开始就必须拆成很多 crate
不一定。
过早拆分会增加结构复杂度。
单 crate 能清楚表达问题时,不必急着多 crate 化。
8.2 误区二:上了 workspace 就等于架构设计好了
不对。
workspace 只是组织手段,不会替你自动消除耦合。
8.3 误区三:monorepo 一定优于多仓库
不一定。
如果组件生命周期差异大、权限边界差异大,monorepo 也可能带来额外治理成本。
8.4 误区四:共享代码越多越好
不对。
共享过度往往会让演化速度下降,并制造隐式依赖。
9. 一个更实用的判断思路
如果你在判断一个 Rust 项目是否该走 workspace / monorepo,可以先问:
- 是否已经有多个 crate 或多个运行单元需要协同维护
- 是否存在明显的共享基础能力需要稳定复用
- 是否需要统一依赖、CI、质量门禁和发布治理
- crate 的边界是否能明确反映架构边界,而不是随意切分
- 当前问题是结构太散,还是其实单 crate 仍然足够清楚
10. 建议学习顺序
建议按这个顺序理解:
- 先区分 crate、package、workspace 各自的角色
- 再理解为什么多 crate 会带来依赖与治理问题
- 再理解 workspace 怎样帮助统一构建、测试与依赖管理
- 再思考共享 crate 的抽象边界应该如何控制
- 最后再进入 monorepo 的 CI、发布与权限治理实践
11. 自测标准
- 能解释单个 crate 与 workspace 的角色差异
- 能理解 monorepo 的价值不只是“放在同一个仓库”
- 能知道 workspace 主要解决依赖统一、共享代码与工程治理问题
- 能意识到 crate 划分应当尽量反映架构边界
- 能判断一个项目当前是应该继续单 crate,还是值得进入 workspace 组织